// ==UserScript==
// @name           Caffeinated StockMonkey
// @namespace      http://www.frieduck.com/sb
// @description    Calculates stock average and stddev, tracks min/max, and highlights high/low stocks. Based on SB Stockmonkey currently maintained by not V.
// @version        0.2
// @include        http://*spybattle.com/brokerage.php*
// @include        http://*spybattle.com/portfolio.php*
// @include        http://*.starpirates.net/brokerage.php*
// @include        http://*.starpirates.net/portfolio.php*
// @author         GIJoe, Theine, Memethief
// @license        http://creativecommons.org/licenses/by-nc-sa/3.0/
// ==/UserScript==

/* CHANGELOG
 *
 * 0.3.0:
 * - Started keeping a changelog
 *
 */
var debugLevel = 1;

var gvar=function() {} // Global variables
function GM_ApiBrowserCheck() {
  const GMSTORAGE_PATH = 'GM_'; // You can change it to avoid conflict with others scripts
  if(typeof(unsafeWindow)=='undefined') { unsafeWindow=window; }
  if(typeof(GM_log)=='undefined') { GM_log=function(msg) { try { unsafeWindow.console.log('GM_log: '+msg); } catch(e) {} }; }
  GM_clog=function(msg) { if(arguments.callee.counter) { arguments.callee.counter++; } else { arguments.callee.counter=1; } GM_log('('+arguments.callee.counter+') '+msg); }
  GM_addGlobalStyle=function(css) { // Redefine GM_addGlobalStyle with a better routine
    var sel=document.createElement('style'); sel.setAttribute('type','text/css'); sel.appendChild(document.createTextNode(css));
    var hel=document.documentElement.firstChild; while(hel && hel.nodeName!='HEAD') { hel=hel.nextSibling; }
    if(hel && hel.nodeName=='HEAD') { hel.appendChild(sel); } else { document.body.insertBefore(sel,document.body.firstChild); }
    return sel;
  }
  var needApiUpgrade=false;
  if(window.navigator.appName.match(/^opera/i) && typeof(window.opera)!='undefined') {
    needApiUpgrade=true; gvar.isOpera=true; GM_log=window.opera.postError; GM_log('Opera detected...');
  }
  if(typeof(GM_setValue)!='undefined') {
    var gsv; try { gsv=GM_setValue.toString(); } catch(e) { gsv='staticArgs'; }
    if(gsv.indexOf('staticArgs')>0) { gvar.isGreaseMonkey=true; debug('GreaseMonkey Api detected...'); } // test GM_hitch
    else if(gsv.match(/not\s+supported/)) { needApiUpgrade=true; gvar.isBuggedChrome=true; debug('Bugged Chrome GM Api detected...'); }
  } else { needApiUpgrade=true; debug('No GM Api detected...'); }

  if(needApiUpgrade) {
    debug('Try to recreate needed GM Api...');
    var ws=null; try { ws=typeof(unsafeWindow.localStorage); unsafeWindow.localStorage.length; } catch(e) { ws=null; } // Catch Security error
    if(ws=='object') {
      debug('Using localStorage for GM Api.');
      GM_getValue=function(name,defValue) { var value=unsafeWindow.localStorage.getItem(GMSTORAGE_PATH+name); if(value==null) { return defValue; } else { switch(value.substr(0,2)) { case 'S]': return value.substr(2); case 'N]': return parseInt(value.substr(2)); case 'B]': return value.substr(2)=='true'; } } return value; }
      GM_setValue=function(name,value) { switch (typeof(value)) { case 'string': unsafeWindow.localStorage.setItem(GMSTORAGE_PATH+name,'S]'+value); break; case 'number': if(value.toString().indexOf('.')<0) { unsafeWindow.localStorage.setItem(GMSTORAGE_PATH+name,'N]'+value); } break; case 'boolean': unsafeWindow.localStorage.setItem(GMSTORAGE_PATH+name,'B]'+value); break; } }
      GM_deleteValue=function(name) { unsafeWindow.localStorage.removeItem(GMSTORAGE_PATH+name); }
    } else if(!gvar.isOpera || typeof(GM_setValue)=='undefined') {
      debug('Using temporarilyStorage for GM Api.'); gvar.temporarilyStorage=new Array();
      GM_getValue=function(name,defValue) { if(typeof(gvar.temporarilyStorage[GMSTORAGE_PATH+name])=='undefined') { return defValue; } else { return gvar.temporarilyStorage[GMSTORAGE_PATH+name]; } }
      GM_setValue=function(name,value) { switch (typeof(value)) { case "string": case "boolean": case "number": gvar.temporarilyStorage[GMSTORAGE_PATH+name]=value; } }
      GM_deleteValue=function(name) { delete gvar.temporarilyStorage[GMSTORAGE_PATH+name]; };
    }
    if(typeof(GM_openInTab)=='undefined') { GM_openInTab=function(url) { unsafeWindow.open(url,""); } }
    if(typeof(GM_registerMenuCommand)=='undefined') { GM_registerMenuCommand=function(name,cmd) { debug("Notice: GM_registerMenuCommand is not supported."); } } // Dummy
    if(!gvar.isOpera || typeof(GM_xmlhttpRequest)=='undefined') {
      debug('Using XMLHttpRequest for GM Api.');
      GM_xmlhttpRequest=function(obj) {
        var request=new XMLHttpRequest();
        request.onreadystatechange=function() { if(obj.onreadystatechange) { obj.onreadystatechange(request); }; if(request.readyState==4 && obj.onload) { obj.onload(request); } }
        request.onerror=function() { if(obj.onerror) { obj.onerror(request); } }
        try { request.open(obj.method,obj.url,true); } catch(e) { if(obj.onerror) { obj.onerror( {readyState:4,responseHeaders:'',responseText:'',responseXML:'',status:403,statusText:'Forbidden'} ); }; return; }
        if(obj.headers) { for(name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
        request.send(obj.data); return request;
  } } }
}
GM_ApiBrowserCheck();

function getGamePrefix() {
	if (document.title.match(/Star/)) {
		return "SP:";
	}
	else if (document.title.match(/Spy/)) {
		return "SB:";
	}
	else {
		//window.alert("Unknown game");
		return "UNK:";
	}
}

// We introduce a weighted average in version 0.3. Here more recent values are
// given a higher weight than old ones; this cuts down on needing to clear
// your data periodically. Roughly speaking, set this to the number of records
// you want to rely on. The smaller the number, the more the calculated
// average will rely on recent readings. To disable this feature, set it to 
// zero.
var limited_history_weight = 20; 

var game_prefix = getGamePrefix();
var last_update_time = parseInt(GM_getValue(game_prefix + 'LastUpdateTime', 0));
var current_time = new Date().getTime();
var updateInterval = 5 * 60 * 1000; // 5 minutes
var shouldUpdate = (last_update_time == 0 || current_time - last_update_time > updateInterval );

addStyles();

if (window.location.href.match(/brokerage\.php/)) {
	buyPage();
}
else if (window.location.href.match(/portfolio\.php/)) {
	sellPage();
}

function buyPage() {
	//fixButtons();

	var can_own = getCanOwn();
	debug("You can purchase " + can_own + " more shares");

	var in_hand = getCash("in-hand");
	var bank = getCash("bank") + in_hand;

	bank = bank + in_hand;
	debug("You have " + bank + " available for purchase");

	var changed = false;

	var price_nodes = xpath("//td[contains(.,'$') and contains(.,'+') and following-sibling::td[descendant::input[@type='hidden' and @name='stocks_id']]]");
	for( var i = 0; i < price_nodes.snapshotLength ; i++) {
		var price_node = price_nodes.snapshotItem(i);
		var stock_name = price_node.previousSibling.textContent.replace(/[^A-Z]/ig,'');
		var price_text = price_node.textContent;
		var price = parseDollars(price_text.split('+')[0]);
		var transactionFee = parseDollars(price_text.split('+')[1]);
		var total_cost = price + transactionFee;
		debug("Processing stock: " + stock_name + " current price " + price);
		
		var stock = getStock(stock_name);
		debug("Current average: " + stock.avg);
		var last = stock.last;

		if ( stock.samples == 0  || shouldUpdate) {
			doUpdate(stock_name,stock,price);	
			changed = true;
		}
		var newHTML = '<table cellpadding=0 cellspacing=0 border=0 width="99%">';
		newHTML += "<tr><td colspan=3 style='font-size:15px;'><b>$"+price+" <small>($"+total_cost+" / $" + Math.round(stock.avg-total_cost)+")</small></b></td></tr>";
		newHTML += "<tr><td style='font-size:11px;'>Min: $"+stock.min+"</td>";
		newHTML += "<td style='font-size:11px;'>Max: $"+stock.max+"</td>";
		newHTML += "<td style='font-size:11px;'>Last: $"+last+"</td></tr>";
		newHTML += "<tr><td style='font-size:11px;'>Avg: $"+stock.avg.toFixed(2)+"</td>";
		newHTML += "<td style='font-size:11px;'><span title='Standard Deviation -- A measure of variability in the stock price.'>SD: $"+ Math.sqrt( (stock.samples > 1) ? stock.s/(stock.samples - 1) : 0.0 ).toFixed(2)+"</span></td>";
		newHTML += "<td style='font-size:11px;'><span title='Number of samples'>n: "+stock.samples+"</span></td></tr>";
		if (limited_history_weight) {
			debug("Using weighted history");
			newHTML += "<tr><td style='font-size:11px;'>wAvg: $"+stock.wavg.toFixed(2)+"</td>";
			newHTML += "<td style='font-size:11px;'><span title='Standard Deviation -- A measure of variability in the stock price.'>wSD: $"+ Math.sqrt( (stock.samples > 1) ? stock.ws/(stock.samples - 1) : 0.0 ).toFixed(2)+"</span></td>";
			newHTML += "<td style='font-size:11px;'><span title='Weighting of historical data'>w: "+limited_history_weight+"</span></td></tr>";		
		} else {
			debug("Using unweighted history");
		}

		var amountBank = Math.floor(bank / total_cost);
		if (amountBank > can_own) {
			amountBank = can_own;
		}

		var amountPull = (total_cost * amountBank) - in_hand;
		if (amountPull < 0) {
			amountPull = 0;
		}

		newHTML += "<tr><td colspan=3 style='font-size:.8em;'>Max Purchasable: "+amountBank + " (withdraw: "+amountPull+")</td></tr>";

		if(stock.avg > price * 1.1) {
			price_node.className += ' buyMe';
			var maxProfit = Math.floor(amountBank * (stock.max - total_cost));
			var avgProfit = Math.floor(amountBank * (stock.avg - total_cost));

			newHTML += "<tr><td colspan=3 style='font-size:.8em;'>Profit Avg: "+avgProfit.toString() +"</td></tr>";
			newHTML += "<tr><td colspan=3 style='font-size:.8em;'>Profit Max: " + maxProfit.toString() +"</td></tr>";
		} else if (stock.avg * 1.1 < price) {
			price_node.className += ' sellMe';
		} else {
			price_node.className += ' holdMe';
		}
		newHTML += '</table>';
		price_node.innerHTML = newHTML;
		

	}

	if (changed) {
		last_update_time = current_time;
	}
	var headerNode = xpath_first_node("//td[@class='contentcontent' and contains(.,'You have $')]");
	var newHTML = "<br><center>Last Updated: " + new Date(last_update_time).toLocaleString() + "</center>";
	newHTML += "<br><center><a href='/portfolio.php'>Jump to View/Sell Stocks</a></center>";
	newHTML += "<br><center><input class='button' type='button' value='Reset Tracking Data' id='resetButton'/></center>";
	headerNode.innerHTML += newHTML;

	var resetAll = function() {
		if(confirm("Really reset StockMonkey Data?")) {
			GM_setValue(game_prefix + 'last_update_time',0);

			window.setTimeout(function() {
				alert("StockMonkey Data has been reset.");
				location.href=unescape(window.location.pathname);
			}, 1);
		}
	};

	var reset_button = document.getElementById("resetButton");
	reset_button.addEventListener(	"click",
					resetAll,
					true);
}

function sellPage() {
	var last_update_time = parseInt(GM_getValue(game_prefix + 'LastUpdateTime', 0));
	var changed = false;;
	
	var price_nodes = xpath("//td[position()=2 and following-sibling::td[child::input[@type='hidden' and @name='stocks_id']]]");
	for( var i = 0; i < price_nodes.snapshotLength ; i++) {
		var price_node = price_nodes.snapshotItem(i);
		var stock_name = price_node.previousSibling.textContent.replace(/[^A-Z]/ig,'');
		var price = parseDollars(price_node.textContent);		
		debug("Processing stock: " + stock_name);
		
		var stock = getStock(stock_name);
		debug("Current average: " + stock.avg);
		var last = stock.last;

		if ( stock.samples == 0  || shouldUpdate) {
			doUpdate(stock_name,stock,price);
			changed = true;
		}		
		var newHTML = '<table cellpadding=0 cellspacing=0 border=0 width="99%">';
		newHTML += "<tr><td colspan=3 style='font-size:15px;'><b>$"+price+"</b></td></tr>";
		newHTML += "<tr><td style='font-size:11px;'>Min: $"+stock.min+"</td>";
		newHTML += "<td style='font-size:11px;'>Max: $"+stock.max+"</td>";
		newHTML += "<td style='font-size:11px;'>Last: $"+last+"</td><td>&nbsp;</td></tr>";
		newHTML += "<tr><td style='font-size:11px;'>Avg: $"+stock.avg.toFixed(2)+"</td>";
		newHTML += "<td style='font-size:11px;'><span title='Standard Deviation -- A measure of variability in the stock price.'>SD: $"+ Math.sqrt( (stock.samples > 1) ? stock.s/(stock.samples - 1) : 0.0 ).toFixed(2)+"</span></td>";
		newHTML += "<td style='font-size:11px;'><span title='Number of samples'>n: "+stock.samples+"</span></td></tr>";		
		if (limited_history_weight) {
			debug("Using weighted history");
			newHTML += "<tr><td style='font-size:11px;'>wAvg: $"+stock.wavg.toFixed(2)+"</td>";
			newHTML += "<td style='font-size:11px;'><span title='Standard Deviation -- A measure of variability in the stock price.'>wSD: $"+ Math.sqrt( (stock.samples > 1) ? stock.ws/(stock.samples - 1) : 0.0 ).toFixed(2)+"</span></td>";
			newHTML += "<td style='font-size:11px;'><span title='Weighting of historical data'>w: "+limited_history_weight+"</span></td></tr>";		
		} else {
			debug("Using unweighted history");
		}
		newHTML += '</table>';
		price_node.innerHTML = newHTML;
		
		if(stock.avg > price * 1.1) {
			price_node.className += ' buyMe';
		} else if (stock.avg * 1.1 < price) {
			price_node.className += ' sellMe';
		} else {
			price_node.className += ' holdMe';
		}
	}
	
	if (changed) {
		last_update_time = current_time;
	}
	var headerNode = xpath_first_node("//td[@class='contentcontent']/b[contains(.,'NOTE: stock purchases')]");
	var newDiv = document.createElement('div');
	newDiv.innerHTML = "<br/><center>Last Updated: " + new Date(last_update_time).toLocaleString() + "</center>";
	headerNode.parentNode.insertBefore(newDiv,headerNode.nextSibling);
}

function doUpdate(stock_name,stock,price) {
	stock.samples++;

	if (stock.samples == 1) {
		stock.min = price;
		stock.max = price;
		stock.avg = price;
		stock.wavg = price;
		stock.last = price;
	}
	else {
		if (price < stock.min) {
			stock.min = price;
		}
		if (price > stock.max) {
			stock.max = price;
		}
		stock.last = price;
		var prev_avg = stock.avg;
		stock.avg = prev_avg  + ((price - prev_avg)/stock.samples);
		var prev_wavg = stock.wavg;
		var effective_samples = (limited_history_weight ? Math.min(limited_history_weight,stock.samples) : stock.samples);
		debug("Using effective number of samples: "+effective_samples);
		stock.wavg = prev_wavg  + ((price - prev_wavg)/effective_samples);
		stock.s = stock.s + ((price - prev_avg)*(price - stock.avg));
		stock.ws = stock.ws + ((price - prev_wavg)*(price - stock.wavg));
	}
	saveStock(stock_name,stock);
}

function addStyles() {
	var head, style;
	head = document.getElementsByTagName('head')[0];
	if (!head) { return; }
	style = document.createElement('style');
	style.type = 'text/css';
	style.appendChild( document.createTextNode( 				
		'.buyMe { background-color: #358811 ! important; '
		+'text-align: left ! important; '
		+'padding: .5em ! important; '
		+'}'
		+ '.sellMe { background-color: #882211 ! important; '
		+'text-align: left ! important; '
		+'padding: .5em ! important; '
		+'}'
		+ '.holdMe { background-color: #116288 ! important; '
		+'text-align: left ! important; '
		+'padding: .5em ! important; '
		+'}' ) ) ;
	head.appendChild(style);
}
							
							
//fix [Buy] and [Buy Max] buttons
function fixButtons() {
	var inputelementamount = document.getElementsByName("amount");

	inputelementamount[0].parentNode.parentNode.parentNode.innerHTML = inputelementamount[0].parentNode.parentNode.parentNode.innerHTML.replace(/<\/form><tr><td>/ig,"</form></td></tr><tr><td>");
	for(var i = 0; i < inputelementamount.length; i++) {
		inputelementamount[i].parentNode.innerHTML = "<form method='post'>" + inputelementamount[i].parentNode.innerHTML;
	}
}

function saveStock(stock_name,stock) {
	GM_setValue(game_prefix + "Stock:" + stock_name,JSON.stringify(stock));
	GM_setValue(game_prefix + 'LastUpdateTime',current_time.toString());
}

/**
	Attempts to load existing record from GM storage
	or creates a new record if one is not found.
	
*/
function getStock(stock_name) {
	var stock;

	if (last_update_time == 0) {
		debug("Never updated. Initializing stock object...");
			stock = {
				samples: 0,
				min: 0,
				max: 0,
				last: 0,
				avg: 0.0,
				s: 0,
				wavg: 0.0,
				ws: 0
			};
	}
	else {
		stock = JSON.parse(GM_getValue(game_prefix + "Stock:" + stock_name,"{}"));
		if (stock.samples == undefined) {
			debug("Stock " + stock_name + " not found");
			stock = {
				samples: 0,
				min: 0,
				max: 0,
				last: 0,
				avg: 0.0,
				s: 0,
				wavg: 0.0,
				ws: 0
			};
		}
		else {
			debug("Stock " + stock_name + " found");
		}
	}
	
	return stock;
}

function getCanOwn() {
	var can_own_tmp;
	
	var can_own_node = xpath_first_node("//td[@class='contentcontent' and contains(.,'You have ')]");
	if (can_own_node) {
		var matches = can_own_node.innerHTML.match(/([0-9]+) shares \(of ([0-9]+) allowed/);
		if (matches) {
			can_own_tmp = (parseInt(matches[2]) - parseInt(matches[1]));
		}
	}

	return can_own_tmp;
}


function getCash(place) {
	var node;
	var amount = 0;
	var action;
	
	if (place == "in-hand") {
		action = "withdraw";
	}
	else if (place == "bank") {
		action = "deposit";
	}
	node = xpath_first_node("//a[contains(@href,'bank.php?" + action + "')]//div[@class='text_a']|//a[contains(@href,'bank.php?" + action + "') and contains(.,'$')]");

	if (node) {
		amount = parseDollars(node.innerHTML);
	}
	
	return amount;
}

function parseDollars(text) {
	var matches = text.match(/\$([0-9,]+)/);
	if (matches) {
		var amount_text = matches[1].replace(/\$/,'').replace(/,/g,'');
		return parseInt(amount_text);
	}
	return NaN;
}	

function xpath(query) {
    return document.evaluate(query, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}

function xpath_first_node(query) {
	return document.evaluate(query, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

/**
 * debug: Check if we allow debug output and, if so, log an entry.
 */
function debug (message) {
	if (debugLevel) { GM_log(message); }
}

